#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <js_native_api.h>
#include <js_native_api_types.h>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <tuple>
#include <unistd.h>
#include <unordered_map>
#include <utility>
#include <vector>

#include "napi/native_api.h" // IWYU pragma: keep

#ifdef NDEBUG
#define ASYNC_SCOPE_ASSERT(cond) ((void)0)
#define VOID_FUNC_ATTR
#else
#define ASYNC_SCOPE_ASSERT(cond) assert(cond)
#define VOID_FUNC_ATTR __attribute__((optnone))
#endif

#define STRICT_NAPI_CALL(call) \
    if (call != napi_ok)       \
    abort()

#define NON_COPYABLE(name)       \
    name(const name &) = delete; \
    name &operator=(const name &) = delete

#define NON_MOVEABLE(name)  \
    name(name &&) = delete; \
    name &operator=(name &&) = delete

#define NON_COPY_NON_MOVE(name) \
    NON_COPYABLE(name);         \
    NON_MOVEABLE(name)

class ThreadSafeTask
{
public:
    typedef void (*ThreadSafeCallback)(napi_env env, void *data);
    static void Init(napi_env env);
    // impl thradsafe function will release whether attached on multi trhreads
    static void Deinit(napi_env env);
    static void Attach(napi_env env);
    static void Detach(napi_env env);
    static bool IsInitializedEnv(napi_env env);
    static uint32_t GetThreadId();
    static ThreadSafeTask Post(napi_env env, ThreadSafeCallback callback, void *data);
    static void Call(napi_env env, ThreadSafeCallback callback, void *data);

    ThreadSafeTask(napi_env env, ThreadSafeCallback callback, void *data = nullptr);
    ~ThreadSafeTask();
    void Wait();

    NON_COPY_NON_MOVE(ThreadSafeTask);

private:
    using ThreadInfo = std::pair<uint32_t, napi_threadsafe_function>;
    struct ThreadSafePack
    {
        ThreadSafePack(ThreadSafeTask *parent, ThreadSafeCallback callback, void *data)
            : parent_(parent), callback_(callback), data_(data)
        {}
        std::atomic<ThreadSafeTask *> parent_;
        ThreadSafeCallback callback_;
        void *data_;
        std::mutex mutex_{};
        std::condition_variable cond_;
        std::atomic<bool> taskDone_{false};
    };
    static napi_threadsafe_function GetHandle(napi_env env, bool &attached);
    static napi_threadsafe_function GetHandleUnsafe(napi_env env, bool &attached);
    static napi_threadsafe_function GetHandleNoThreadCheckUnsafe(napi_env env, bool &attached, uint32_t &threadId);
    static void Cleanup(void *data);
    static void Finalize(napi_env env, void *data, void *hint);
    static void Execute(napi_env env, napi_value func, void *context, void *data);
    static std::shared_mutex mutex_;
    static std::unordered_map<napi_env, ThreadInfo> handles_;

    std::atomic<ThreadSafePack *> pack_{nullptr};
};
std::shared_mutex ThreadSafeTask::mutex_{};
std::unordered_map<napi_env, ThreadSafeTask::ThreadInfo> ThreadSafeTask::handles_{};

bool ThreadSafeTask::IsInitializedEnv(napi_env env)
{
    bool attached;
    GetHandle(env, attached);
    return attached;
}

napi_threadsafe_function ThreadSafeTask::GetHandleNoThreadCheckUnsafe(napi_env env, bool &attached, uint32_t &threadId)
{
    auto info = handles_.find(env);
    attached = false;
    threadId = 0;
    if (info == handles_.end())
        return nullptr;
    attached = true;
    threadId = info->second.first;
    return info->second.second;
}

napi_threadsafe_function ThreadSafeTask::GetHandleUnsafe(napi_env env, bool &attached)
{
    uint32_t threadId = 0;
    napi_threadsafe_function tsfn = GetHandleNoThreadCheckUnsafe(env, attached, threadId);
    if (threadId == GetThreadId())
        return nullptr;
    return tsfn;
}

napi_threadsafe_function ThreadSafeTask::GetHandle(napi_env env, bool &attached)
{
    std::shared_lock lock(mutex_);
    return GetHandleUnsafe(env, attached);
}

ThreadSafeTask::ThreadSafeTask(napi_env env, ThreadSafeCallback callback, void *data)
{
    bool attached = false;
    auto tsfn = GetHandle(env, attached);

    if (!attached)
        abort();

    if (tsfn == nullptr)
    {
        callback(env, data);
        return;
    }

    pack_ = new ThreadSafePack(this, callback, data);
    STRICT_NAPI_CALL(napi_call_threadsafe_function(tsfn, pack_, napi_tsfn_blocking));
}

ThreadSafeTask::~ThreadSafeTask()
{
    auto pack = pack_.exchange(nullptr);
    if (pack != nullptr)
        pack->parent_ = nullptr;
}

VOID_FUNC_ATTR void ThreadSafeTask::Wait()
{
    auto pack = pack_.load();
    if (pack == nullptr || pack->taskDone_.load())
    {
        return;
    }
    [[maybe_unused]] std::unique_lock lock(pack->mutex_);
    pack->cond_.wait(lock, [&] { return pack->taskDone_.load(); });
}

ThreadSafeTask ThreadSafeTask::Post(napi_env env, ThreadSafeCallback callback, void *data)
{
    return ThreadSafeTask(env, callback, data);
}

VOID_FUNC_ATTR void ThreadSafeTask::Call(napi_env env, ThreadSafeCallback callback, void *data)
{
    ThreadSafeTask(env, callback, data).Wait();
}

VOID_FUNC_ATTR void ThreadSafeTask::Init(napi_env env)
{
    [[maybe_unused]] std::lock_guard lock(mutex_);
    if (handles_.find(env) != handles_.end())
        return;

    napi_value taskName = nullptr;
    // maybe use string instead, async_resource_name now.
    STRICT_NAPI_CALL(napi_get_undefined(env, &taskName));
    napi_threadsafe_function tsfn = nullptr;
    STRICT_NAPI_CALL(napi_create_threadsafe_function(env, nullptr, nullptr, taskName, 0, 1, nullptr, Finalize, nullptr,
                                                     Execute, &tsfn));
    auto result = handles_.insert({env, {GetThreadId(), tsfn}});
    STRICT_NAPI_CALL(napi_add_env_cleanup_hook(env, Cleanup, tsfn));
    // failed to insert
    if (!result.second)
        abort();
}
VOID_FUNC_ATTR void ThreadSafeTask::Deinit(napi_env env)
{
    [[maybe_unused]] std::lock_guard lock(mutex_);
    bool attached = false;
    auto tsfn = GetHandle(env, attached);
    if (!attached)
        return;
    handles_.erase(env);
    STRICT_NAPI_CALL(napi_release_threadsafe_function(tsfn, napi_tsfn_abort));
}
VOID_FUNC_ATTR void ThreadSafeTask::Attach(napi_env env)
{
    bool attached = false;
    auto tsfn = GetHandle(env, attached);
    if (attached && tsfn != nullptr)
    {
        STRICT_NAPI_CALL(napi_acquire_threadsafe_function(tsfn));
    }
}
VOID_FUNC_ATTR void ThreadSafeTask::Detach(napi_env env)
{
    [[maybe_unused]] std::lock_guard lock(mutex_);
    bool attached = false;
    uint32_t threadId = 0;
    auto tsfn = GetHandleNoThreadCheckUnsafe(env, attached, threadId);
    if (!attached)
        return;
    STRICT_NAPI_CALL(napi_release_threadsafe_function(tsfn, napi_tsfn_release));
}
VOID_FUNC_ATTR void ThreadSafeTask::Execute(napi_env env, napi_value func, void *context, void *data)
{
    if (data == nullptr)
        abort();
    auto pack = reinterpret_cast<ThreadSafePack *>(data);
    if (pack == nullptr)
        abort();
    {
        [[maybe_unused]] std::lock_guard lock(pack->mutex_);
        pack->callback_(env, pack->data_);
        pack->taskDone_ = true;
        pack->cond_.notify_all();
    }
    auto parent = pack->parent_.exchange(nullptr);
    if (parent != nullptr)
        parent->pack_ = nullptr;
    delete pack;
}

VOID_FUNC_ATTR void ThreadSafeTask::Cleanup(void *data)
{
    STRICT_NAPI_CALL(
        napi_release_threadsafe_function(reinterpret_cast<napi_threadsafe_function>(data), napi_tsfn_abort));
}

VOID_FUNC_ATTR void ThreadSafeTask::Finalize(napi_env env, void *data, void *hint)
{
    [[maybe_unused]] std::unique_lock lock(mutex_);
    auto result = handles_.find(env);
    ASYNC_SCOPE_ASSERT(result != handles_.end());
    STRICT_NAPI_CALL(napi_remove_env_cleanup_hook(env, Cleanup, result->second.second));
    handles_.erase(result);
}

uint32_t ThreadSafeTask::GetThreadId()
{
#if defined(__amd64__) || defined(__x86_64__)
    static uint32_t tid = gettid();
    return tid;
#else
    return gettid();
#endif
}

static char16_t KNapiString_CharAt(napi_env env, napi_ref ref, uint32_t index)
{
    std::vector<char16_t> buffer(index + 2, 0); // 2: index to length add zero
    size_t written = 0;
    auto data = std::tuple<napi_ref, std::vector<char16_t> &, size_t &>(ref, buffer, written);
    ThreadSafeTask::Call(
        env,
        [](napi_env env, void *data) {
            auto args = *reinterpret_cast<std::tuple<napi_ref, std::vector<char16_t> &, size_t &> *>(data);
            napi_value str = nullptr;
            STRICT_NAPI_CALL(napi_get_reference_value(env, std::get<0>(args), &str));
            auto &buffer = std::get<1>(args);
            auto &written = std::get<2>(args);
            STRICT_NAPI_CALL(napi_get_value_string_utf16(env, str, buffer.data(), buffer.size(), &written));
        },
        &data);
    return written < index ? 0 : buffer[index];
}

constexpr const int GET_TID_COUNT = 1000000;

static napi_value Hello(napi_env env, napi_callback_info info)
{
    return nullptr;
}

static napi_value Detach(napi_env env, napi_callback_info info)
{
    ThreadSafeTask::Detach(env);
    return nullptr;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    ThreadSafeTask::Init(env);

    std::thread([env] {
        std::cout << "called thread: " << ThreadSafeTask::GetThreadId() << std::endl;
        ThreadSafeTask::Post(
            env,
            [](napi_env env, void *data) {
                std::cout << "recv thread: " << ThreadSafeTask::GetThreadId() << std::endl;
            },
            nullptr);
    }).detach();

    std::vector<napi_property_descriptor> desc{
        {"hello", nullptr, Hello, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"detach", nullptr, Detach, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, desc.size(), desc.data());
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "async",
    .nm_priv = ((void *)0),
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    napi_module_register(&demoModule);
}
